﻿@using System.Collections.Specialized;
@using System.Collections;
@using System.Globalization;
@using System.Security.Cryptography;
@using System.Security.Cryptography.X509Certificates;
@using System.Text;
@using System.Web.WebPages.Scope;
@using System.Xml.Linq;
@functions
{
    /** Private constants **/
    private const string NullMessage = "Argument cannot be null or empty";
    private const string ProductionUrl = "https://payments.amazon.com/checkout/";
    private const string SandboxUrl = "https://payments-sandbox.amazon.com/checkout/";

    /** Private readonly properties **/
    private static readonly XNamespace ns = "http://payments.amazon.com/checkout/2009-05-15/";
    private static readonly object merchantId = new object();
    private static readonly object awsAccessKeyId = new object();
    private static readonly object secretKeyId = new object(); 
    private static readonly object x509Certificate = new object();
    private static readonly object useSandbox = new object();
    private static readonly object associateTag = new object();
    private static readonly object initialized = new object();
    private static readonly object currencyCode = new object();
    private static readonly object lengthUnitOfMeasure = new object();
    private static readonly object volumeUnitOfMeasure = new object();
    private static readonly object weightUnitOfMeasure = new object();
    private static readonly object jewelryWeightUnitOfMeasure = new object();
    private static readonly object degreeUnitOfMeasure = new object();
    private static readonly object memorySizeUnitOfMeasure = new object();
    private static readonly object frequencyUnitOfMeasure = new object();
    private static readonly object amperageUnitOfMeasure = new object();
    private static readonly object timeUnitOfMeasure = new object();
    private static readonly object dateUnitOfMeasure = new object();
    private static readonly object assemblyTimeUnitOfMeasure = new object();
    private static readonly object ageRecommendedUnitOfMeasure = new object();
    private static readonly object batteryPowerUnitOfMeasure = new object();
    private static readonly object voltageUnitOfMeasure = new object();
    private static readonly object wattageUnitOfMeasure = new object();
    private static readonly object temperatureRatingUnitOfMeasure = new object();
    private static readonly object clothingSizeUnitOfMeasure = new object();

    /** Public readonly properties **/
    public static readonly IEnumerable<string> AvailableCurrencyCodes = new[] { "USD", "GBP", "EUR", "JPY", "CAD" };
    public static readonly IEnumerable<string> AvailableLengthUnitOfMeasures = new[] { "MM", "CM", "M", "IN", "FT" };
    public static readonly IEnumerable<string> AvailableVolumeUnitOfMeasures = new[] { "cubic-cm", "cubic-ft", "cubic-in", "cubic-m", "cubic-yd", "cup", "gallon", "liter", "milliliter", "ounce", "pint", "quart" };
    public static readonly IEnumerable<string> AvailableWeightUnitOfMeasures = new[] { "GR", "KG", "OZ", "LB" };
    public static readonly IEnumerable<string> AvailableJewelryWeightUnitOfMeasures = new[] { "GR", "KG", "OZ", "LB", "CARATS", "DWT" };
    public static readonly IEnumerable<string> AvailableDegreeUnitOfMeasures = new[] { "degrees" };
    public static readonly IEnumerable<string> AvailableMemorySizeUnitOfMeasures = new[] { "TB", "GB", "MB", "KB" };
    public static readonly IEnumerable<string> AvailableFrequencyUnitOfMeasures = new[] { "KHz", "MHz", "GHz" };
    public static readonly IEnumerable<string> AvailableAmperageUnitOfMeasures = new[] { "amps", "milliamps" };
    public static readonly IEnumerable<string> AvailableTimeUnitOfMeasures = new[] { "sec", "min", "hr" };
    public static readonly IEnumerable<string> AvailableDateUnitOfMeasures = new[] { "days", "weeks", "months", "years" };
    public static readonly IEnumerable<string> AvailableAssemblyTimeUnitOfMeasures = new[] { "minutes", "hours", "days", "weeks", "months", "years" };
    public static readonly IEnumerable<string> AvailableAgeRecommendedUnitOfMeasures = new[] { "months", "years" };
    public static readonly IEnumerable<string> AvailableBatteryPowerUnitOfMeasures = new[] { "mAh" };
    public static readonly IEnumerable<string> AvailableVoltageUnitOfMeasures = new[] { "volts" };
    public static readonly IEnumerable<string> AvailableWattageUnitOfMeasures = new[] { "watts" };
    public static readonly IEnumerable<string> AvailableTemperatureRatingUnitOfMeasures = new[] { "degrees-celsius", "degrees-fahrenheit" };
    public static readonly IEnumerable<string> AvailableClothingSizeUnitOfMeasures = new[] { "IN", "CM" };

    public static readonly IEnumerable<string> AvailableCategoriesForUpselling = new string[] 
    { 
        "Apparel", "Automotive", "Baby", "Beauty", "Books", 
        "Classical", "DVD", "Electronics", "Gourmet Food", "Grocery", 
        "Health & Personal Care", "Home & Garden", "Industrial", "Jewelry", 
        "Kitchen", "Magazines", "Music", "Musical Instruments", 
        "Office Products", "PC Hardware", "Pet Supplies", "Photo", 
        "Software", "Sporting Goods", "Tools", "Toys", "VHS", "Video", 
        "Watches", "Wireless Accessories" 
    };
                            
    public static readonly Dictionary<string, string> AvailablePredefinedRegions = new Dictionary<string, string>() 
    {
        { "USContinental48States", "US Continental 48 States" },
        { "USFull50States", "US Full 50 States" },
        { "USAll", "US All" },
        { "WorldAll", "World All" }
    };

    public static readonly Dictionary<string, string> AvailableServiceLevels = new Dictionary<string, string>() 
    {
        { "0", "Standard" },
        { "1", "Expedited" },
        { "2", "One Day" },
        { "3", "Two Days" }
    };
    
    public static readonly Dictionary<string, string> AvailableUSStates = new Dictionary<string, string>() 
    {
        { "AL", "Alabama" }, 
        { "AK", "Alaska" }, 
        { "AZ", "Arizona" }, 
        { "AR", "Arkansas" }, 
        { "CA", "California" }, 
        { "CO", "Colorado" }, 
        { "CT", "Connecticut" }, 
        { "DE", "Delaware" }, 
        { "DC", "District of Columbia" }, 
        { "FL", "Florida" }, 
        { "GA", "Georgia" }, 
        { "HI", "Hawaii" }, 
        { "ID", "Idaho" }, 
        { "IL", "Illinois" }, 
        { "IN", "Indiana" }, 
        { "IA", "Iowa" }, 
        { "KS", "Kansas" }, 
        { "KY", "Kentucky" }, 
        { "LA", "Louisiana" }, 
        { "ME", "Maine" }, 
        { "MD", "Maryland" }, 
        { "MA", "Massachusetts" }, 
        { "MI", "Michigan" }, 
        { "MN", "Minnesota" }, 
        { "MS", "Mississippi" }, 
        { "MO", "Missouri" }, 
        { "MT", "Montana" }, 
        { "NE", "Nebraska" }, 
        { "NV", "Nevada" }, 
        { "NH", "New Hampshire" }, 
        { "NJ", "New Jersey" }, 
        { "NM", "New Mexico" }, 
        { "NY", "New York" }, 
        { "NC", "North Carolina" }, 
        { "ND", "North Dakota" }, 
        { "OH", "Ohio" }, 
        { "OK", "Oklahoma" }, 
        { "OR", "Oregon" }, 
        { "PA", "Pennsylvania" }, 
        { "RI", "Rhode Island" }, 
        { "SC", "South Carolina" }, 
        { "SD", "South Dakota" }, 
        { "TN", "Tennessee" }, 
        { "TX", "Texas" }, 
        { "UT", "Utah" }, 
        { "VT", "Vermont" }, 
        { "VA", "Virginia" }, 
        { "WA", "Washington" }, 
        { "WV", "West Virginia" }, 
        { "WI", "Wisconsin" }, 
        { "WY", "Wyoming" }
    };

    public static readonly Dictionary<string, string> AvailableCountries = new Dictionary<string, string>() 
    {
        { "AF", "Afghanistan" }, 
        { "AX", "Åland Islands" }, 
        { "AL", "Albania" }, 
        { "DZ", "Algeria" }, 
        { "AS", "American Samoa" }, 
        { "AD", "Andorra" }, 
        { "AO", "Angola" }, 
        { "AI", "Anguilla" }, 
        { "AQ", "Antarctica" }, 
        { "AG", "Antigua and Barbuda" }, 
        { "AR", "Argentina" }, 
        { "AM", "Armenia" }, 
        { "AW", "Aruba" }, 
        { "AU", "Australia" }, 
        { "AT", "Austria" }, 
        { "AZ", "Azerbaijan" }, 
        { "BS", "Bahamas" }, 
        { "BH", "Bahrain" }, 
        { "BD", "Bangladesh" }, 
        { "BB", "Barbados" }, 
        { "BY", "Belarus" }, 
        { "BE", "Belgium" }, 
        { "BZ", "Belize" }, 
        { "BJ", "Benin" }, 
        { "BM", "Bermuda" }, 
        { "BT", "Bhutan" }, 
        { "BO", "Bolivia" }, 
        { "BA", "Bosnia and Herzegovina" }, 
        { "BW", "Botswana" }, 
        { "BV", "Bouvet Island" }, 
        { "BR", "Brazil" }, 
        { "IO", "British Indian Ocean Territory" }, 
        { "BN", "Brunei" }, 
        { "BG", "Bulgaria" }, 
        { "BF", "Burkina Faso" }, 
        { "BI", "Burundi" }, 
        { "KH", "Cambodia" }, 
        { "CM", "Cameroon" }, 
        { "CA", "Canada" }, 
        { "CV", "Cape Verde" }, 
        { "KY", "Cayman Islands" }, 
        { "CF", "Central African Republic" }, 
        { "TD", "Chad" }, 
        { "CL", "Chile" }, 
        { "CN", "China" }, 
        { "CX", "Christmas Island" }, 
        { "CC", "Cocos Islands" }, 
        { "CO", "Colombia" }, 
        { "KM", "Comoros" }, 
        { "CG", "Congo" }, 
        { "CD", "Congo, the Democratic Republic of the" }, 
        { "CK", "Cook Islands" }, 
        { "CR", "Costa Rica" }, 
        { "CI", "Côte d'Ivoire" }, 
        { "HR", "Croatia" }, 
        { "CU", "Cuba" }, 
        { "CY", "Cyprus" }, 
        { "CZ", "Czech Republic" }, 
        { "DK", "Denmark" }, 
        { "DJ", "Djibouti" }, 
        { "DM", "Dominica" }, 
        { "DO", "Dominican Republic" }, 
        { "EC", "Ecuador" }, 
        { "EG", "Egypt" }, 
        { "SV", "El Salvador" }, 
        { "GQ", "Equatorial Guinea" }, 
        { "ER", "Eritrea" }, 
        { "EE", "Estonia" }, 
        { "ET", "Ethiopia" }, 
        { "FK", "Falkland Islands" }, 
        { "FO", "Faroe Islands" }, 
        { "FJ", "Fiji" }, 
        { "FI", "Finland" }, 
        { "FR", "France" }, 
        { "GF", "French Guiana" }, 
        { "PF", "French Polynesia" }, 
        { "TF", "French Southern Territories" }, 
        { "GA", "Gabon" }, 
        { "GM", "Gambia" }, 
        { "GE", "Georgia" }, 
        { "DE", "Germany" }, 
        { "GH", "Ghana" }, 
        { "GI", "Gibraltar" }, 
        { "GR", "Greece" }, 
        { "GL", "Greenland" }, 
        { "GD", "Grenada" }, 
        { "GP", "Guadeloupe" }, 
        { "GU", "Guam" }, 
        { "GT", "Guatemala" }, 
        { "GN", "Guinea" }, 
        { "GW", "Guinea-Bissau" }, 
        { "GY", "Guyana" }, 
        { "HT", "Haiti" }, 
        { "HM", "Heard Island And McDonald Islands" }, 
        { "HN", "Honduras" }, 
        { "HK", "Hong Kong" }, 
        { "HU", "Hungary" }, 
        { "IS", "Iceland" }, 
        { "IN", "India" }, 
        { "ID", "Indonesia" }, 
        { "IR", "Iran" }, 
        { "IQ", "Iraq" }, 
        { "IE", "Ireland" }, 
        { "IL", "Israel" }, 
        { "IT", "Italy" }, 
        { "JM", "Jamaica" }, 
        { "JP", "Japan" }, 
        { "JO", "Jordan" }, 
        { "KZ", "Kazakhstan" }, 
        { "KE", "Kenya" }, 
        { "KI", "Kiribati" }, 
        { "KP", "Korea, Demoratic People's Republic of" }, 
        { "KR", "Korea, Republic of" }, 
        { "KW", "Kuwait" }, 
        { "KG", "Kyrgyzstan" }, 
        { "LA", "Laos" }, 
        { "LV", "Latvia" }, 
        { "LB", "Lebanon" }, 
        { "LS", "Lesotho" }, 
        { "LR", "Liberia" }, 
        { "LY", "Libya" }, 
        { "LI", "Liechtenstein" }, 
        { "LT", "Lithuania" }, 
        { "LU", "Luxembourg" }, 
        { "MO", "Macao" }, 
        { "MK", "Macedonia" }, 
        { "MG", "Madagascar" }, 
        { "MW", "Malawi" }, 
        { "MY", "Malaysia" }, 
        { "MV", "Maldives" }, 
        { "ML", "Mali" }, 
        { "MT", "Malta" }, 
        { "MH", "Marshall Islands" }, 
        { "MQ", "Martinique" }, 
        { "MR", "Mauritania" }, 
        { "MU", "Mauritius" }, 
        { "YT", "Mayotte" }, 
        { "MX", "Mexico" }, 
        { "FM", "Micronesia" }, 
        { "MD", "Moldova" }, 
        { "MC", "Monaco" }, 
        { "MN", "Mongolia" }, 
        { "MS", "Montserrat" }, 
        { "MA", "Morocco" }, 
        { "MZ", "Mozambique" }, 
        { "MM", "Myanmar" }, 
        { "NA", "Namibia" }, 
        { "NR", "Nauru" }, 
        { "NP", "Nepal" }, 
        { "NL", "Netherlands" }, 
        { "AN", "Netherlands Antilles" }, 
        { "NC", "New Caledonia" }, 
        { "NZ", "New Zealand" }, 
        { "NI", "Nicaragua" }, 
        { "NE", "Niger" }, 
        { "NG", "Nigeria" }, 
        { "NU", "Niue" }, 
        { "NF", "Norfolk Island" }, 
        { "MP", "Northern Mariana Islands" }, 
        { "NO", "Norway" }, 
        { "OM", "Oman" }, 
        { "PK", "Pakistan" }, 
        { "PW", "Palau" }, 
        { "PS", "Palestine" }, 
        { "PA", "Panama" }, 
        { "PG", "Papua New Guinea" }, 
        { "PY", "Paraguay" }, 
        { "PE", "Peru" }, 
        { "PH", "Philippines" }, 
        { "PN", "Pitcairn" }, 
        { "PL", "Poland" }, 
        { "PT", "Portugal" }, 
        { "PR", "Puerto Rico" }, 
        { "QA", "Qatar" }, 
        { "RE", "Reunion" }, 
        { "RO", "Romania" }, 
        { "RU", "Russia" }, 
        { "RW", "Rwanda" }, 
        { "SH", "Saint Helena" }, 
        { "KN", "Saint Kitts And Nevis" }, 
        { "LC", "Saint Lucia" }, 
        { "PM", "Saint Pierre And Miquelon" }, 
        { "VC", "Saint Vincent And The Grenadines" }, 
        { "WS", "Samoa" }, 
        { "SM", "San Marino" }, 
        { "ST", "Sao Tome And Principe" }, 
        { "SA", "Saudi Arabia" }, 
        { "SN", "Senegal" }, 
        { "CS", "Serbia and Montenegro" }, 
        { "SC", "Seychelles" }, 
        { "SL", "Sierra Leone" }, 
        { "SG", "Singapore" }, 
        { "SK", "Slovakia" }, 
        { "SI", "Slovenia" }, 
        { "SB", "Solomon Islands" }, 
        { "SO", "Somalia" }, 
        { "ZA", "South Africa" }, 
        { "GS", "South Georgia And The South Sandwich Islands" }, 
        { "ES", "Spain" }, 
        { "LK", "Sri Lanka" }, 
        { "SD", "Sudan" }, 
        { "SR", "Suriname" }, 
        { "SJ", "Svalbard And Jan Mayen" }, 
        { "SZ", "Swaziland" }, 
        { "SE", "Sweden" }, 
        { "CH", "Switzerland" }, 
        { "SY", "Syria" }, 
        { "TW", "Taiwan" }, 
        { "TJ", "Tajikistan" }, 
        { "TZ", "Tanzania" }, 
        { "TH", "Thailand" }, 
        { "TL", "Timor-Leste" }, 
        { "TG", "Togo" }, 
        { "TK", "Tokelau" }, 
        { "TO", "Tonga" }, 
        { "TT", "Trinidad and Tobago" }, 
        { "TN", "Tunisia" }, 
        { "TR", "Turkey" }, 
        { "TM", "Turkmenistan" }, 
        { "TC", "Turks And Caicos Islands" }, 
        { "TV", "Tuvalu" }, 
        { "UG", "Uganda" }, 
        { "UA", "Ukraine" }, 
        { "AE", "United Arab Emirates" }, 
        { "GB", "United Kingdom" }, 
        { "US", "United States" }, 
        { "UM", "United States Minor Outlying Islands" }, 
        { "UY", "Uruguay" }, 
        { "UZ", "Uzbekistan" }, 
        { "VU", "Vanuatu" }, 
        { "VA", "Vatican" }, 
        { "VE", "Venezuela" }, 
        { "VN", "Vietnam" }, 
        { "VG", "Virgin Islands, British" }, 
        { "VI", "Virgin Islands, U.S." }, 
        { "WF", "Wallis And Futuna" }, 
        { "EH", "Western Sahara" }, 
        { "YE", "Yemen" }, 
        { "ZM", "Zambia" }, 
        { "ZW", "Zimbabwe" } 
    };

    /** Public properties **/
    public static string CheckOutUrl
    {
        get { return UseSandbox ? SandboxUrl + MerchantId : ProductionUrl + MerchantId; }
    }

    public static bool UseSandbox
    {
        get { return (bool)(ScopeStorage.CurrentScope[useSandbox] ?? false); }
        private set { ScopeStorage.CurrentScope[useSandbox] = value; }
    }

    public static string SecretKeyId
    {
        get { return (string)(ScopeStorage.CurrentScope[secretKeyId] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[secretKeyId] = value; }
    }

    public static string AwsAccessKeyId
    {
        get { return (string)(ScopeStorage.CurrentScope[awsAccessKeyId] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[awsAccessKeyId] = value; }
    }

    public static X509Certificate2 X509Certificate
    {
        get { return (X509Certificate2)(ScopeStorage.CurrentScope[x509Certificate]); }
        private set { ScopeStorage.CurrentScope[x509Certificate] = value; }
    }
    
    public static string AssociateTag
    {
        get { return (string)(ScopeStorage.CurrentScope[associateTag] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[associateTag] = value; }
    }

    public static string MerchantId
    {
        get
        {
            EnsureInitalized();
            return (string)(ScopeStorage.CurrentScope[merchantId] ?? "");
        }

        private set { ScopeStorage.CurrentScope[merchantId] = value; }
    }

    public static string CurrencyCode
    {
        get { return (string)(ScopeStorage.CurrentScope[currencyCode] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[currencyCode] = value; }
    }

    public static string LengthUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[lengthUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[lengthUnitOfMeasure] = value; }
    }

    public static string VolumeUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[volumeUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[volumeUnitOfMeasure] = value; }
    }

    public static string WeightUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[weightUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[weightUnitOfMeasure] = value; }
    }

    public static string JewelryWeightUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[jewelryWeightUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[jewelryWeightUnitOfMeasure] = value; }
    }

    public static string DegreeUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[degreeUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[degreeUnitOfMeasure] = value; }
    }

    public static string MemorySizeUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[memorySizeUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[memorySizeUnitOfMeasure] = value; }
    }

    public static string FrequencyUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[frequencyUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[frequencyUnitOfMeasure] = value; }
    }

    public static string AmperageUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[amperageUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[amperageUnitOfMeasure] = value; }
    }

    public static string TimeUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[timeUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[timeUnitOfMeasure] = value; }
    }

    public static string DateUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[dateUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[dateUnitOfMeasure] = value; }
    }

    public static string AssemblyTimeUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[assemblyTimeUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[assemblyTimeUnitOfMeasure] = value; }
    }

    public static string AgeRecommendedUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[ageRecommendedUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[ageRecommendedUnitOfMeasure] = value; }
    }

    public static string BatteryPowerUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[batteryPowerUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[batteryPowerUnitOfMeasure] = value; }
    }

    public static string VoltageUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[voltageUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[voltageUnitOfMeasure] = value; }
    }

    public static string WattageUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[wattageUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[wattageUnitOfMeasure] = value; }
    }

    public static string TemperatureRatingUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[temperatureRatingUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[temperatureRatingUnitOfMeasure] = value; }
    }

    public static string ClothingSizeUnitOfMeasure
    {
        get { return (string)(ScopeStorage.CurrentScope[clothingSizeUnitOfMeasure] ?? string.Empty); }
        private set { ScopeStorage.CurrentScope[clothingSizeUnitOfMeasure] = value; }
    }

    /*** Private Properties ***/
    private static bool Initialized
    {
        get { return (bool)(ScopeStorage.CurrentScope[initialized] ?? false); }
        set { ScopeStorage.CurrentScope[initialized] = value; }
    }
    
    /***  Public access points  ***/
    public static void Initialize(
                                    string merchantId, string awsAccessKeyId = null, string secretKeyId = null, X509Certificate2 x509Certificate = null,
                                    string associateTag = null, bool useSandbox = true,
                                    string currencyCode = "USD",
                                    string lengthUnitOfMeasure = "IN",
                                    string volumeUnitOfMeasure = "cubic-cm",
                                    string weightUnitOfMeasure = "LB",
                                    string jewelryWeightUnitOfMeasure = "LB",
                                    string degreeUnitOfMeasure = "degrees",
                                    string memorySizeUnitOfMeasure = "GB",
                                    string frequencyUnitOfMeasure = "MHz",
                                    string amperageUnitOfMeasure = "amps",
                                    string timeUnitOfMeasure = "sec",
                                    string dateUnitOfMeasure = "days",
                                    string assemblyTimeUnitOfMeasure = "minutes",
                                    string ageRecommendedUnitOfMeasure = "months",
                                    string batteryPowerUnitOfMeasure = "mAh",
                                    string voltageUnitOfMeasure = "volts",
                                    string wattageUnitOfMeasure = "watts",
                                    string temperatureRatingUnitOfMeasure = "degrees-celsius",
                                    string clothingSizeUnitOfMeasure = "IN")
    {
        if (String.IsNullOrEmpty(merchantId))
            throw new ArgumentException(NullMessage, "merchantId");

        AmazonCheckout.MerchantId = merchantId;
        AmazonCheckout.AwsAccessKeyId = awsAccessKeyId;
        AmazonCheckout.SecretKeyId = secretKeyId;
        AmazonCheckout.X509Certificate = x509Certificate;
        AmazonCheckout.AssociateTag = associateTag;
        AmazonCheckout.UseSandbox = useSandbox;
        AmazonCheckout.CurrencyCode = currencyCode;
        AmazonCheckout.LengthUnitOfMeasure = lengthUnitOfMeasure;
        AmazonCheckout.VolumeUnitOfMeasure = volumeUnitOfMeasure;
        AmazonCheckout.WeightUnitOfMeasure = weightUnitOfMeasure;
        AmazonCheckout.JewelryWeightUnitOfMeasure = jewelryWeightUnitOfMeasure;
        AmazonCheckout.DegreeUnitOfMeasure = degreeUnitOfMeasure;
        AmazonCheckout.MemorySizeUnitOfMeasure = memorySizeUnitOfMeasure;
        AmazonCheckout.FrequencyUnitOfMeasure = frequencyUnitOfMeasure;
        AmazonCheckout.AmperageUnitOfMeasure = amperageUnitOfMeasure;
        AmazonCheckout.TimeUnitOfMeasure = timeUnitOfMeasure;
        AmazonCheckout.DateUnitOfMeasure = dateUnitOfMeasure;
        AmazonCheckout.AssemblyTimeUnitOfMeasure = assemblyTimeUnitOfMeasure;
        AmazonCheckout.AgeRecommendedUnitOfMeasure = ageRecommendedUnitOfMeasure;
        AmazonCheckout.BatteryPowerUnitOfMeasure = batteryPowerUnitOfMeasure;
        AmazonCheckout.VoltageUnitOfMeasure = voltageUnitOfMeasure;
        AmazonCheckout.WattageUnitOfMeasure = wattageUnitOfMeasure;
        AmazonCheckout.TemperatureRatingUnitOfMeasure = temperatureRatingUnitOfMeasure;
        AmazonCheckout.ClothingSizeUnitOfMeasure = clothingSizeUnitOfMeasure;
        AmazonCheckout.Initialized = true;
    }

    public static HelperResult GetAmazonButton(
                                                string name,
                                                string description,
                                                decimal price,
                                                string imageUrl,
                                                double? weight = null,
                                                string categoryName = null,
                                                string promotionId = null,
                                                string condition = null,
                                                string taxTableId = null,
                                                string imageButtonUrl = "https://payments.amazon.com/gp/cba/button?ie=UTF8&color=orange&background=white&size=medium",
                                                string alternativeButtonText = "Checkout with Amazon Payments",
                                                int quantity = 1,
                                                string sku = "",
                                                bool expressCheckout = true,
                                                IEnumerable<string> upsellingItems = null,
                                                IEnumerable<string> inclusionCategories = null,
                                                IEnumerable<string> exclusionCategories = null)
    {
        var singleItemCart = AmazonCheckout.CreateAmazonCart()
                                      .AddItem(name, description, price, imageUrl, quantity, sku, weight, categoryName, promotionId, condition, taxTableId, shippingMethodIds: new string[] { })
                                      .AddUpsellingItems(upsellingItems)
                                      .AddInclusionCategories(inclusionCategories)
                                      .AddExclusionCategories(exclusionCategories);

        return AmazonCheckout.GetAmazonButton(singleItemCart, imageButtonUrl, alternativeButtonText, expressCheckout);
    }

    // Returns and empty cart to be used for adding items before showing an amazon checkout button.
    public static AmazonCart CreateAmazonCart()
    {
        return new AmazonCart();
    }

    public class AmazonCart
    {
        private readonly List<AmazonItem> items = new List<AmazonItem>();
        private readonly List<string> upsellingItems = new List<string>();
        private readonly List<string> inclusionCategories = new List<string>();
        private readonly List<string> exclusionCategories = new List<string>();
        private readonly PromotionCollection promotions = new PromotionCollection();
        private readonly TaxTableCollection taxTables = new TaxTableCollection();
        private readonly TaxTable defaultTaxTable = new DefaultTaxTableWrapper();
        private readonly ShippingMethodsCollection shippingMethods = new ShippingMethodsCollection();

        public string ClientRequestId { get; set; }
        public List<AmazonItem> Items { get { return this.items; } }
        public List<string> UpsellingItems { get { return this.upsellingItems; } }
        public List<string> InclusionCategories { get { return this.inclusionCategories; } }
        public List<string> ExclusionCategories { get { return this.exclusionCategories; } }
        public PromotionCollection Promotions { get { return this.promotions; } }
        public TaxTable DefaultTaxTable { get { return this.defaultTaxTable; } }
        public TaxTableCollection TaxTables { get { return this.taxTables; } }
        public ShippingMethodsCollection ShippingMethods { get { return this.shippingMethods; } }

        public Promotion CartPromotion
        {
            get { return this.Promotions.CartPromotion; }
            set { this.Promotions.CartPromotion = value; }
        }

        /// <summary>
        /// Adds Items to the cart
        /// </summary>       
        public AmazonCart AddItem(string name, string description, decimal price, string imageUrl, int quantity = 1, string sku = "", double? weight = null, string categoryName = null, string promotionId = null, string condition = null, string taxTableId = null, IEnumerable<string> shippingMethodIds = null)
        {
            if (!string.IsNullOrWhiteSpace(promotionId) && this.CartPromotion != null)
                throw new InvalidOperationException("You cannot assign both cart and item-level discounts. These options are mutually exclusive.");

            AmazonItem ai = new AmazonItem();
            ai.Title = name;
            ai.Price = price;
            ai.Description = description;
            ai.Sku = sku;
            ai.Quantity = quantity;
            ai.ImageUrl = imageUrl;
            ai.Weight = weight;
            ai.CategoryName = categoryName;
            ai.PromotionId = promotionId;
            ai.Condition = condition;
            ai.TaxTableId = taxTableId;

            if (shippingMethodIds != null)
                foreach (string shipMethodId in shippingMethodIds)
                    ai.WithShippingMethod(shipMethodId);

            this.items.Add(ai);
            return this;

        }

        public AmazonCart AddUpsellingItems(IEnumerable<string> items)
        {
            if (items == null)
                return this;
            
            if (string.IsNullOrEmpty(AmazonCheckout.AssociateTag) && items.Any())
                throw new ArgumentException("AssociateTag must be provided for Up-selling items.");

            if (items != null)
                this.upsellingItems.AddRange(items);

            return this;
        }

        public AmazonCart AddInclusionCategories(IEnumerable<string> items)
        {
            if (items == null)
                return this;
            
            if (string.IsNullOrEmpty(AmazonCheckout.AssociateTag) && items.Any())
                throw new ArgumentException("AssociateTag must be provided for Up-selling items.");

            if (items != null)
                this.inclusionCategories.AddRange(items);

            return this;
        }

        public AmazonCart AddExclusionCategories(IEnumerable<string> items)
        {
            if (items == null)
                return this;
            
            if (string.IsNullOrEmpty(AmazonCheckout.AssociateTag) && items.Any())
                throw new ArgumentException("AssociateTag must be provided for Up-selling items.");

            if (items != null)
                this.exclusionCategories.AddRange(items);

            return this;
        }

        public AmazonCart WithFixedAmountDiscount(string promotionId, string description, decimal amount)
        {
            if (this.Items.Any(item => !string.IsNullOrWhiteSpace(item.PromotionId)))
                throw new InvalidOperationException("You cannot assign both cart and item-level discounts. These options are mutually exclusive.");
       
            this.CartPromotion = new FixedAmountDiscount(promotionId)
            {
                Description = description,
                Amount = amount,
                CurrencyCode = AmazonCheckout.CurrencyCode
            };

            return this;
        }

        public AmazonCart WithDiscountRate(string promotionId, string description, decimal value)
        {
            if (this.Items.Any(item => !string.IsNullOrWhiteSpace(item.PromotionId)))
                throw new InvalidOperationException("You cannot assign both cart and item-level discounts. These options are mutually exclusive.");

            this.CartPromotion = new DiscountRate(promotionId)
            {
                Description = description,
                Value = value
            };

            return this;
        }

        public XDocument ToXml()
        {
            var document = new XDocument(new XElement(ns + "Order"));

            if (!string.IsNullOrWhiteSpace(this.ClientRequestId))
                document.Root.Add(new XElement(ns + "ClientRequestId", this.ClientRequestId));

            var itemsElement = new XElement(ns + "Items");

            // Items
            var cartElement = new XElement(ns + "Cart", itemsElement);
            this.Items.ForEach(item => itemsElement.Add(item.ToXml()));

            if (this.CartPromotion != null)
            {
                cartElement.Add(new XElement(ns + "CartPromotionId", this.CartPromotion.Id));
            }

            document.Root.Add(cartElement);
            document.Root.Add(this.DefaultTaxTable.ToXml());
            document.Root.Add(this.TaxTables.ToXml());
            document.Root.Add(this.Promotions.ToXml());
            document.Root.Add(this.ShippingMethods.ToXml());

            // Up selling items
            if (!string.IsNullOrEmpty(@AmazonCheckout.AssociateTag) && (this.UpsellingItems.Count() > 0 || this.InclusionCategories.Count() > 0 || this.ExclusionCategories.Count() > 0))
            {
                var upsellPreferences = new XElement(ns + "AmazonUpsellPreferences");
                upsellPreferences.Add(new XElement(ns + "AssociateTag", @AmazonCheckout.AssociateTag));

                if (this.ExclusionCategories.Any())
                {
                    var categories = new XElement(ns + "ExclusionCategories");
                    this.ExclusionCategories.ForEach(c => categories.Add(new XElement(ns + "Category", c)));
                    upsellPreferences.Add(categories);
                }

                if (this.InclusionCategories.Any())
                {
                    var categories = new XElement(ns + "InclusionCategories");
                    this.InclusionCategories.ForEach(c => categories.Add(new XElement(ns + "Category", c)));
                    upsellPreferences.Add(categories);
                }

                if (this.UpsellingItems.Any())
                {
                    var sellItems = new XElement(ns + "AmazonUpsellItems");
                    this.UpsellingItems.ForEach(ui => sellItems.Add(new XElement(ns + "ASIN", ui)));
                    upsellPreferences.Add(sellItems);
                }

                document.Root.Add(upsellPreferences);
            }

            return document;
        }

        private class DefaultTaxTableWrapper : TaxTable
        {
        }
    }

    public class AmazonItem
    {
        private readonly List<string> shippingMethods = new List<string>();

        public string Title { get; set; }
        public decimal Price { get; set; }
        public string Sku { get; set; }
        public int Quantity { get; set; }
        public string Description { get; set; }
        public string ImageUrl { get; set; }
        public double? Weight { get; set; }
        public string CategoryName { get; set; }
        public string PromotionId { get; set; }
        public string Condition { get; set; }
        public string TaxTableId { get; set; }

        public IEnumerable<string> AmazonShippingMethodIds
        {
            get { return this.shippingMethods; }
        }

        public string FulfillmentNetwork { get; set; }

        public AmazonItem WithShippingMethod(string id)
        {
            this.shippingMethods.Add(id);

            return this;
        }

        public virtual XElement ToXml()
        {
            var newItem = new XElement(ns + "Item",
                    string.IsNullOrWhiteSpace(this.Sku) ? null : new XElement(ns + "SKU", this.Sku),
                    new XElement(ns + "MerchantId", @AmazonCheckout.MerchantId),
                    new XElement(ns + "Title", this.Title),
                    new XElement(ns + "Description", this.Description),
                    new XElement(ns + "Price", new XElement(ns + "Amount", this.Price.ToString(CultureInfo.InvariantCulture)), new XElement(ns + "CurrencyCode", @AmazonCheckout.CurrencyCode)),
                    new XElement(ns + "Quantity", this.Quantity));

            if (this.Weight != null)
                newItem.Add(new XElement(ns + "Weight",
                                new XElement(ns + "Amount", this.Weight),
                                new XElement(ns + "Unit", @AmazonCheckout.WeightUnitOfMeasure)));

            if (!string.IsNullOrWhiteSpace(this.CategoryName))
                newItem.Add(new XElement(ns + "Category", this.CategoryName));

            if (!string.IsNullOrWhiteSpace(this.TaxTableId))
                newItem.Add(new XElement(ns + "TaxTableId", this.TaxTableId));

            if (!string.IsNullOrWhiteSpace(this.PromotionId))
                newItem.Add(new XElement(ns + "PromotionIds", new XElement(ns + "PromotionId", this.PromotionId)));

            if (this.AmazonShippingMethodIds.Any())
            {
                var shippingMethods = new XElement(ns + "ShippingMethodIds");
                foreach (var id in this.AmazonShippingMethodIds)
                    shippingMethods.Add(new XElement(ns + "ShippingMethodId", id));

                newItem.Add(shippingMethods);
            }

            if (!string.IsNullOrWhiteSpace(this.FulfillmentNetwork))
                newItem.Add(new XElement(ns + "FulfillmentNetwork", this.FulfillmentNetwork));

            if (!string.IsNullOrWhiteSpace(this.ImageUrl))
                newItem.Add(new XElement(ns + "Images",
                                new XElement(ns + "Image",
                                    new XElement(ns + "URL", this.ImageUrl))));

            return newItem;
        }
    }

    /********************************************************************************************************
     * Promotion override rules support
     ********************************************************************************************************/
    public abstract class Promotion
    {
        public string Id { get; private set; }
        public string Description { get; set; }
        public abstract XElement ToXml();

        public Promotion(string id)
        {
            this.Id = id;
        }
    }

    public class FixedAmountDiscount : Promotion
    {
        public FixedAmountDiscount(string id)
            : base(id)
        {
        }

        public decimal Amount { get; set; }
        public string CurrencyCode { get; set; }

        public override XElement ToXml()
        {
            return new XElement(ns + "Promotion",
                                new XElement(ns + "PromotionId", this.Id),
                                new XElement(ns + "Description", this.Description),
                                new XElement(ns + "Benefit",
                                    new XElement(ns + "FixedAmountDiscount",
                                        new XElement(ns + "Amount", this.Amount.ToString(CultureInfo.InvariantCulture)),
                                        new XElement(ns + "CurrencyCode", this.CurrencyCode))));
        }
    }

    public class DiscountRate : Promotion
    {
        public DiscountRate(string id)
            : base(id)
        {
        }

        public decimal Value { get; set; }

        public override XElement ToXml()
        {
            return new XElement(ns + "Promotion",
                                new XElement(ns + "PromotionId", this.Id),
                                new XElement(ns + "Description", this.Description),
                                new XElement(ns + "Benefit",
                                    new XElement(ns + "DiscountRate", this.Value)));
        }
    }

    public class PromotionCollection : List<Promotion>
    {
        public Promotion CartPromotion { get; set; }

        public void AddFixedAmountDiscount(string id, string description, decimal amount)
        {
            this.Add(
                new FixedAmountDiscount(id)
                {
                    Description = description,
                    Amount = amount,
                    CurrencyCode = AmazonCheckout.CurrencyCode
                });
        }

        public void AddDiscountRate(string id, string description, decimal value)
        {
            this.Add(
                new DiscountRate(id)
                {
                    Description = description,
                    Value = value
                });
        }

        public virtual XElement ToXml()
        {
            if (!(this.Any() || this.CartPromotion != null))
                return null;

            var promotions = new XElement(ns + "Promotions");
            this.ForEach(item => promotions.Add(item.ToXml()));

            if (this.CartPromotion != null)
                promotions.Add(this.CartPromotion.ToXml());

            return promotions;
        }
    }

    /********************************************************************************************************
     * Tax rule overrides support
     ********************************************************************************************************/
    public enum AmazonPredefinedRegion
    {
        USContinental48States,
        USFull50States,
        USAll,
        WorldAll
    }

    public enum AmazonRegionTypes
    {
        USStateRegion,
        USZipRegion,
        PredefinedRegion,
        CountryRegion,
        PostalRegion
    }

    public abstract class Region
    {       
        public abstract string FriendlyName { get; }

        public abstract string GetValue();

        public string GetDisplayValue()
        {
            var valueSource = this.DisplayValueSource();

            if (valueSource == null) 
                return this.GetValue();
            
            return this.DisplayValueSource().Where(e => e.Key.Equals(this.GetValue())).FirstOrDefault().Value;
        }

        public abstract void SetValue(string value);

        public abstract Dictionary<string, string> DisplayValueSource();
        
        public abstract XElement ToXml();
        
        public static Region Create(string regionType, string regionData)
        {
            var region = (Region)Activator.CreateInstance(Type.GetType("Amazon.Checkout.Helpers.AmazonCheckout+" + regionType));
            region.SetValue(regionData);
            return region;
        } 
    }

    public class USStateRegion : Region
    {
        public string StateCode { get; set; }

        public override string FriendlyName { get { return "US State Region"; } }
        
        public override XElement ToXml()
        {
            return new XElement(ns + AmazonRegionTypes.USStateRegion.ToString(), this.StateCode);
        }
        
        public override string GetValue()
        {
            return this.StateCode;
        }

        public override void SetValue(string value)
        {
            this.StateCode = value;
        }

        public override Dictionary<string, string> DisplayValueSource()
        {
            return AvailableUSStates;
        }
    }

    public class USZipRegion : Region
    {
        public static readonly int MaxZipCodeLength = 10;

        public string ZipCode { get; set; }

        public override string FriendlyName { get { return "US Zip Region"; } }

        public override XElement ToXml()
        {
            return new XElement(ns + AmazonRegionTypes.USZipRegion.ToString(), this.ZipCode);
        }
        
        public override string GetValue()
        {
            return this.ZipCode;
        }

        public override void SetValue(string value)
        {
            this.ZipCode = value;
        }

        public override Dictionary<string, string> DisplayValueSource()
        {
            return null;
        }
    }

    public class PredefinedRegion : Region
    {
        public AmazonPredefinedRegion Id { get; set; }

        public override string FriendlyName { get { return "Predefined Region"; } }

        public override XElement ToXml()
        {
            return new XElement(ns + AmazonRegionTypes.PredefinedRegion.ToString(), this.Id);
        }
        
        public override string GetValue()
        {
            return this.Id.ToString();
        }

        public override void SetValue(string value)
        {
            this.Id = (AmazonPredefinedRegion)Enum.Parse(typeof(AmazonPredefinedRegion), value); ;
        }

        public override Dictionary<string, string> DisplayValueSource()
        {
            return AvailablePredefinedRegions;
        }
    }

    public class CountryRegion : Region
    {
        public string Code { get; set; }

        public override string FriendlyName { get { return "Country Region";  } }
        
        public override XElement ToXml()
        {
            return new XElement(ns + "WorldRegion",
                        new XElement(ns + "CountryCode", this.Code));
        }
        
        public override string GetValue()
        {
            return this.Code;
        }

        public override void SetValue(string value)
        {
            this.Code = value;
        }

        public override Dictionary<string, string> DisplayValueSource()
        {
            return AvailableCountries;
        }
    }

    public class PostalRegion : Region
    {
        public string Code { get; set; }

        public override string FriendlyName { get { return "Postal Region"; } }

        public override XElement ToXml()
        {
            return new XElement(ns + "WorldRegion",
                        new XElement(ns + AmazonRegionTypes.PostalRegion.ToString(), this.Code));
        }
        
        public override string GetValue()
        {
            return this.Code;
        }

        public override void SetValue(string value)
        {
            this.Code = value;
        }

        public override Dictionary<string, string> DisplayValueSource()
        {
            return null;
        }
    }

    public class TaxRule
    {
        public Region Region { get; set; }
        public decimal Rate { get; set; }
        public bool IsShippingTaxed { get; set; }

        public virtual XElement ToXml()
        {
            return new XElement(ns + "TaxRule",
                                new XElement(ns + "Rate", this.Rate),
                                new XElement(ns + "IsShippingTaxed", this.IsShippingTaxed),
                                this.Region.ToXml());
        }
    }

    public class TaxTable : List<TaxRule> 
    {
        public const decimal MinimumTaxRate = 0.0m;
        public const decimal MaximumTaxRate = 0.15m;

        public string Id { get; internal set; }

        protected TaxTable()
        {
            this.Id = null;
        }

        public TaxTable(string id)
        {
            if (string.IsNullOrWhiteSpace(id))
                throw new ArgumentException("Must specify a valid identifier for the tax table.", "id");

            this.Id = id;
        }

        public TaxTable AddRuleForUSZipRegion(string zipCode, decimal rate, bool isShippingTaxed = false)
        {
            if (string.IsNullOrWhiteSpace(zipCode) || zipCode.Length > USZipRegion.MaxZipCodeLength)
                throw new ArgumentException(string.Format("The length of the zip code cannot be greater than {0}.", USZipRegion.MaxZipCodeLength), "zipCode");

            if (rate < MinimumTaxRate || rate > MaximumTaxRate)
                throw new ArgumentException(String.Format("The tax rate {0} is out of range. It should be greater than {1} and less than or equal to {2}.", rate, MinimumTaxRate, MaximumTaxRate), "rate");

            this.Add(new TaxRule()
            {
                Rate = rate,
                IsShippingTaxed = isShippingTaxed,
                Region = new USZipRegion() { ZipCode = zipCode }
            });

            return this;
        }

        public TaxTable AddRuleForPredefinedRegion(AmazonPredefinedRegion region, decimal rate, bool isShippingTaxed = false)
        {
            if (!Enum.IsDefined(typeof(AmazonPredefinedRegion), region))
                throw new ArgumentException("The specified region is invalid.", "region");

            if (rate < MinimumTaxRate || rate > MaximumTaxRate)
                throw new ArgumentException(String.Format("The tax rate {0} is out of range. It should be greater than {1} and less than or equal to {2}.", rate, MinimumTaxRate, MaximumTaxRate), "rate");

            this.Add(new TaxRule()
            {
                Rate = rate,
                IsShippingTaxed = isShippingTaxed,
                Region = new PredefinedRegion() { Id = region }
            });

            return this;
        }

        public TaxTable AddRuleForUSState(string stateCode, decimal rate, bool isShippingTaxed = false)
        {
            if (rate < MinimumTaxRate || rate > MaximumTaxRate)
                throw new ArgumentException(String.Format("The tax rate {0} is out of range. It should be greater than {1} and less than or equal to {2}.", rate, MinimumTaxRate, MaximumTaxRate), "rate");

            this.Add(new TaxRule()
            {
                Rate = rate,
                IsShippingTaxed = isShippingTaxed,
                Region = new USStateRegion() { StateCode = stateCode }
            });

            return this;
        }

        public TaxTable AddRuleForPostalRegion(string postalRegion, decimal rate, bool isShippingTaxed = false)
        {
            if (rate < MinimumTaxRate || rate > MaximumTaxRate)
                throw new ArgumentException(String.Format("The tax rate {0} is out of range. It should be greater than {1} and less than or equal to {2}.", rate, MinimumTaxRate, MaximumTaxRate), "rate");

            this.Add(new TaxRule()
            {
                Rate = rate,
                IsShippingTaxed = isShippingTaxed,
                Region = new PostalRegion() { Code = postalRegion }
            });

            return this;
        }

        public TaxTable AddRuleForCountry(string countryCode, decimal rate, bool isShippingTaxed = false)
        {
            if (rate < MinimumTaxRate || rate > MaximumTaxRate)
                throw new ArgumentException(String.Format("The tax rate {0} is out of range. It should be greater than {1} and less than or equal to {2}.", rate, MinimumTaxRate, MaximumTaxRate), "rate");

            this.Add(new TaxRule()
            {
                Rate = rate,
                IsShippingTaxed = isShippingTaxed,
                Region = new CountryRegion() { Code = countryCode }
            });

            return this;
        }

        public virtual XElement ToXml()
        {
            if (!this.Any())
                return null;

            var taxTable = new XElement(ns + (this.Id == null ? "DefaultTaxTable" : "TaxTable"));

            taxTable.Add(new XElement(ns + "TaxTableId", this.Id));

            var taxRules = new XElement(ns + "TaxRules");
            this.ForEach(item => taxRules.Add(item.ToXml()));
            taxTable.Add(taxRules);

            return taxTable;
        }
    }

    public class TaxTableCollection : List<TaxTable>
    {
        public TaxTable Add(string id)
        {
            var table = new TaxTable(id);
            this.Add(table);

            return table;
        }

        public virtual XElement ToXml()
        {
            if (!this.Any())
                return null;

            var taxTables = new XElement(ns + "TaxTables");
            foreach (TaxTable table in this)
                if (table.Any())
                    taxTables.Add(table.ToXml());

            return taxTables.HasElements ? taxTables : null;
        }
    }

    /********************************************************************************************************
     * Shipment rule overrides support
     ********************************************************************************************************/

    public enum ServiceLevel
    {
        Standard,
        Expedited,
        OneDay,
        TwoDay
    }

    public class ShippingMethod
    {
        private readonly List<Region> includedRegions = new List<Region>();
        private readonly List<Region> excludedRegions = new List<Region>();

        public string Id { get; private set; }
        public ServiceLevel ServiceLevel { get; set; }
        public decimal ShipmentBasedShippingRate { get; set; }
        public decimal WeightBasedShippingRate { get; set; }
        public decimal ItemQuantityBasedShippingRate { get; set; }
        public bool IsPOBoxSupported { get; set; }
        public string DisplayableShippingLabel { get; set; }

        public ShippingMethod(string id, string label = "", bool isPOBoxSupported = false)
        {
            this.Id = id;
            this.DisplayableShippingLabel = label;
            this.IsPOBoxSupported = isPOBoxSupported;
            this.ServiceLevel = AmazonCheckout.ServiceLevel.Standard;
        }

        public IEnumerable<Region> IncludedRegions
        {
            get { return this.includedRegions; }
        }

        public IEnumerable<Region> ExcludedRegions
        {
            get { return this.excludedRegions; }
        }

        public ShippingMethod IncludeUSZipRegion(string zipCode)
        {
            if (string.IsNullOrWhiteSpace(zipCode) || zipCode.Length > USZipRegion.MaxZipCodeLength)
                throw new ArgumentException(string.Format("The length of the zip code cannot be greater than {0}.", USZipRegion.MaxZipCodeLength), "zipCode");

            this.includedRegions.Add(new USZipRegion() { ZipCode = zipCode });
            return this;
        }

        public ShippingMethod IncludePredefinedRegion(AmazonPredefinedRegion region)
        {
            if (!Enum.IsDefined(typeof(AmazonPredefinedRegion), region))
                throw new ArgumentException("The specified region is invalid.", "region");

            this.includedRegions.Add(new PredefinedRegion() { Id = region });
            return this;
        }

        public ShippingMethod IncludeUSStateRegion(string stateCode)
        {
            this.includedRegions.Add(new USStateRegion() { StateCode = stateCode });
            return this;
        }

        public ShippingMethod IncludePostalRegion(string postalRegion)
        {
            this.includedRegions.Add(new PostalRegion() { Code = postalRegion });
            return this;
        }

        public ShippingMethod IncludeCountryRegion(string countryCode)
        {
            this.includedRegions.Add(new CountryRegion() { Code = countryCode });
            return this;
        }

        public ShippingMethod ExcludeUSZipRegion(string zipCode)
        {
            if (string.IsNullOrWhiteSpace(zipCode) || zipCode.Length > USZipRegion.MaxZipCodeLength)
                throw new ArgumentException(string.Format("The length of the zip code cannot be greater than {0}.", USZipRegion.MaxZipCodeLength), "zipCode");

            this.excludedRegions.Add(new USZipRegion() { ZipCode = zipCode });
            return this;
        }

        public ShippingMethod ExcludePredefinedRegion(AmazonPredefinedRegion region)
        {
            if (!Enum.IsDefined(typeof(AmazonPredefinedRegion), region))
                throw new ArgumentException("The specified region is invalid.", "region");

            this.excludedRegions.Add(new PredefinedRegion() { Id = region });

            return this;
        }

        public ShippingMethod ExcludeUSStateRegion(string stateCode)
        {
            this.excludedRegions.Add(new USStateRegion() { StateCode = stateCode });
            return this;
        }

        public ShippingMethod ExcludePostalRegion(string postalRegion)
        {
            this.excludedRegions.Add(new PostalRegion() { Code = postalRegion });
            return this;
        }

        public ShippingMethod ExcludeCountryRegion(string countryCode)
        {
            this.excludedRegions.Add(new CountryRegion() { Code = countryCode });
            return this;
        }

        public ShippingMethod WithServiceLevel(ServiceLevel serviceLevel)
        {
            this.ServiceLevel = serviceLevel;
            return this;
        }

        public ShippingMethod WithShipmentBasedShippingRate(decimal rate)
        {
            this.ShipmentBasedShippingRate = rate;
            return this;
        }

        public ShippingMethod WithWeightBasedShippingRate(decimal rate)
        {
            this.WeightBasedShippingRate = rate;
            return this;
        }

        public ShippingMethod WithItemQuantityBasedShippingRate(decimal rate)
        {
            this.ItemQuantityBasedShippingRate = rate;
            return this;
        }

        public virtual XElement ToXml()
        {
            XElement includes = null;

            if (this.includedRegions.Any())
            {
                includes = new XElement(ns + "IncludedRegions");
                this.includedRegions.ForEach(item => includes.Add(item.ToXml()));
            }

            XElement excludes = null;

            if (this.excludedRegions.Any())
            {
                excludes = new XElement(ns + "ExcludedRegions");
                this.excludedRegions.ForEach(item => excludes.Add(item.ToXml()));
            }

            XElement rate = new XElement(ns + "Rate");

            if (this.WeightBasedShippingRate > 0.0m)
                rate.Add(new XElement(ns + "WeightBased",
                            new XElement(ns + "Amount", this.WeightBasedShippingRate.ToString(CultureInfo.InvariantCulture)),
                            new XElement(ns + "CurrencyCode", AmazonCheckout.CurrencyCode)));

            if (this.ItemQuantityBasedShippingRate > 0.0m)
                rate.Add(new XElement(ns + "ItemQuantityBased",
                            new XElement(ns + "Amount", this.ItemQuantityBasedShippingRate.ToString(CultureInfo.InvariantCulture)),
                            new XElement(ns + "CurrencyCode", AmazonCheckout.CurrencyCode)));

            if (this.ShipmentBasedShippingRate > 0.0m)
                rate.Add(new XElement(ns + "ShipmentBased",
                            new XElement(ns + "Amount", this.ShipmentBasedShippingRate.ToString(CultureInfo.InvariantCulture)),
                            new XElement(ns + "CurrencyCode", AmazonCheckout.CurrencyCode)));

            return new XElement(ns + "ShippingMethod",
                        new XElement(ns + "ShippingMethodId", this.Id),
                        new XElement(ns + "ServiceLevel", this.ServiceLevel.ToString()),
                        rate,
                        includes,
                        excludes,
                        new XElement(ns + "IsPOBoxSupported", this.IsPOBoxSupported),
                        new XElement(ns + "DisplayableShippingLabel", this.DisplayableShippingLabel));
        }
    }

    public class ShippingMethodsCollection : List<ShippingMethod>
    {
        public ShippingMethod Add(string id, string label = "", bool isPOBoxSupported = false)
        {
            var shippingMethod = new ShippingMethod(id, label, isPOBoxSupported);
            this.Add(shippingMethod);

            return shippingMethod;
        }

        public virtual XElement ToXml()
        {
            if (!this.Any())
                return null;

            var shippingMethods = new XElement(ns + "ShippingMethods");
            foreach (ShippingMethod shippingMethod in this)
                shippingMethods.Add(shippingMethod.ToXml());

            return shippingMethods.HasElements ? shippingMethods : null;
        }
    }

    /* Private Methods */
    private static void EnsureInitalized()
    {
        if (!Initialized)
            throw new InvalidOperationException("The AmazonCheckout Helper has not been initialized.");
    }

    private static String CalculateRFC2104HMAC(String data, String key)
    {
        String result = null;
        HMACSHA1 hmac = new HMACSHA1(ASCIIEncoding.UTF8.GetBytes(key));
        byte[] hash = hmac.ComputeHash(ASCIIEncoding.UTF8.GetBytes(data));
        result = System.Convert.ToBase64String(hash);

        return result;
    }

    private static String CalculateRFC2104HMAC(string data, X509Certificate2 cert)
    {
        var rsa = cert.PrivateKey as RSACryptoServiceProvider;
        var signedData = rsa.SignData(ASCIIEncoding.UTF8.GetBytes(data), new SHA1CryptoServiceProvider());
        return Convert.ToBase64String(signedData);
    }
}
@helper GetAmazonButton(
                        AmazonCheckout.AmazonCart shoppingCart,
                        string imageButtonUrl = "https://payments.amazon.com/gp/cba/button?ie=UTF8&color=orange&background=white&size=medium",
                        string alternativeButtonText = "Checkout with Amazon Payments",
                        bool expressCheckout = true)
{
    var expressCheckoutScriptsAdded = Context.Items["expressCheckoutScriptsAdded"] as bool?;

    if (expressCheckout && expressCheckoutScriptsAdded != true)
    {
        <script type="text/javascript" src="https://images-na.ssl-images-amazon.com/images/G/01/cba/js/jquery.js"></script>        
        <script type="text/javascript" src="@String.Format("https://images-na.ssl-images-amazon.com/images/G/01/cba/js/widget{0}/widget.js", AmazonCheckout.UseSandbox ? "/sandbox" : string.Empty)"></script> 

        Context.Items["expressCheckoutScriptsAdded"] = true;
    }
    <form method="post" action="@AmazonCheckout.CheckOutUrl">
    @{
        var rawXmlCart = shoppingCart.ToXml().ToString();
        var encodedXmlCart = Convert.ToBase64String(Encoding.UTF8.GetBytes(rawXmlCart));

        if (AmazonCheckout.X509Certificate != null)
        {
            var signature = CalculateRFC2104HMAC(rawXmlCart, AmazonCheckout.X509Certificate);
            var publicKey = Convert.ToBase64String(AmazonCheckout.X509Certificate.Export(X509ContentType.Cert));
                            
            <input type="hidden" 
                    name="order-input" 
                    value="type:merchant-signed-order/aws-keypair/1;order:@encodedXmlCart;signature:@signature;certificate:@publicKey" />

        }
        else
        {
            var signature = CalculateRFC2104HMAC(rawXmlCart, @AmazonCheckout.SecretKeyId);
        
            <input type="hidden" 
                        name="order-input" 
                        value="type:merchant-signed-order/aws-accesskey/1;order:@encodedXmlCart;signature:@signature;aws-access-key-id:@AmazonCheckout.AwsAccessKeyId" />
        }
        
    }
        <input type="image" src="@imageButtonUrl" alt="@alternativeButtonText" />
    </form>
}
@helper GetShoppingCart(Func<dynamic, HelperResult> minimizedTemplate = null, string cartAligment = "right", bool expressCheckout = true)
    {
    Context.Items["usingShoppingCart"] = true;

    var expressCheckoutScriptsAdded = Context.Items["expressCheckoutScriptsAdded"] as bool?;
    
    <script type="text/javascript" src="https://images-na.ssl-images-amazon.com/images/G/01/cba/js/jquery.js"></script>
    
    if (expressCheckout && expressCheckoutScriptsAdded != true)
    {
        <script type="text/javascript" src="@String.Format("https://images-na.ssl-images-amazon.com/images/G/01/cba/js/widget{0}/widget.js", AmazonCheckout.UseSandbox ? "/sandbox" : string.Empty)"></script> 
        Context.Items["expressCheckoutScriptsAdded"] = true;
    }
    
    <script type="text/javascript" src="https://images-na.ssl-images-amazon.com/images/G/01/cba/js/common/cba_shared.js"></script>
    <script type="text/javascript" src="@String.Format("https://images-na.ssl-images-amazon.com/images/G/01/cba/js/shoppingcart{0}/merchant_cart.js", AmazonCheckout.UseSandbox ? "/prod-sandbox" : string.Empty)"></script>    
    
    <div id="globalParameters">
        <input type="hidden" name="weight_unit" value="@AmazonCheckout.WeightUnitOfMeasure" />
        <input type="hidden" name="currency_code" value="@AmazonCheckout.CurrencyCode" />
        <input type="hidden" name="merchant_id" value="@AmazonCheckout.MerchantId" />
    </div>    
    
    <div id="merchantConfigurations">
        <input type="hidden" name="cart_alignment" value="@cartAligment"/>
    </div>

    <div class="amazonPaymentsCart">
        <div class="amazonPaymentsCart_Header">
            @if (minimizedTemplate != null)
            {
                @minimizedTemplate(null)
            }
        </div>
    </div>
}